[BE] SISC1-86/87 사용자 베팅 등록/취소 API 구현#56
Hidden character warning
Conversation
Walkthrough사용자 베팅 생성/취소 엔드포인트와 컨트롤러 경로 재정렬(/api, /bet-rounds)이 추가되었고, UserBetRequest DTO·UserBetRepository·서비스 로직(post/cancel)·트랜잭션 경계(REQUIRES_NEW) 및 ErrorCode 확장이 도입되었으며 단위 및 트랜잭션 테스트가 추가/확장되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as 사용자
participant C as BettingController
participant S as BettingService
participant BR as BetRoundRepository
participant UBR as UserBetRepository
participant PH as PointHistoryService
U->>C: POST /api/user-bets (UserBetRequest)
C->>S: postUserBet(userId, req)
S->>BR: findById(req.roundId)
BR-->>S: BetRound / 없음
alt invalid (not found / time / duplicate)
S-->>C: ApiException(ErrorCode.*)
C-->>U: 4xx
else valid
opt paid bet
S->>PH: createPointHistory(-stakePoints, ORIGIN_BET)
PH-->>S: PointHistory
end
S->>UBR: save(UserBet)
UBR-->>S: UserBet
S-->>C: UserBet
C-->>U: 200 OK
end
sequenceDiagram
autonumber
actor U as 사용자
participant C as BettingController
participant S as BettingService
participant UBR as UserBetRepository
participant PH as PointHistoryService
U->>C: DELETE /api/user-bets/{userBetId}
C->>S: cancelUserBet(userId, userBetId)
S->>UBR: findByUserBetIdAndUserId(id, userId)
UBR-->>S: UserBet / 없음
alt not found / round closed
S-->>C: ApiException(ErrorCode.*)
C-->>U: 4xx
else ok
opt paid bet
S->>PH: createPointHistory(+stakePoints, ORIGIN_BET_CANCEL)
PH-->>S: PointHistory
end
S->>UBR: delete(UserBet)
S-->>C: Void
C-->>U: 204 No Content
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Comment |
|
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (6)
backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java (1)
60-65: 무료 베팅 테스트 케이스 추가 권장현재 테스트는 유료 베팅(
free=false,stakePoints=100) 시나리오만 다룹니다. 무료 베팅 시나리오에서 포인트 히스토리가 호출되지 않아야 함을 검증하는 테스트 케이스를 추가하는 것이 좋습니다.backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (2)
137-139: 시간 비교 로직 일관성 개선 권장Line 97에서는
now.isBefore()및now.isAfter()를 사용하는 반면, Line 137에서는now.isAfter()만 사용합니다. 코드 일관성을 위해 동일한 패턴을 사용하는 것이 좋습니다.- if (LocalDateTime.now().isAfter(betRound.getLockAt())){ + LocalDateTime now = LocalDateTime.now(); + if (now.isAfter(betRound.getLockAt())){ throw new CustomException(ErrorCode.BET_ROUND_CLOSED); }
147-147: 포인트 히스토리 originId 일관성 확보
베팅 생성(create) 시userBetRequest.getRoundId()(라인 107), 취소(cancel) 시userBet.getUserBetId()(라인 147)를 사용 중입니다. 두 경우 동일한 식별자를 사용하거나 역할을 명확히 문서화하세요.backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java (3)
237-259: 시간 기반 테스트의 안정성 개선을 권장합니다.
LocalDateTime.now()를 사용하면 시간 경계 부근에서 테스트가 불안정해질 수 있습니다.고정된 시간을 사용하도록 리팩토링하는 것을 권장합니다:
private BetRound openRoundNow() { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime fixedTime = LocalDateTime.of(2025, 10, 13, 12, 0); return BetRound.builder() .betRoundID(roundId) .scope(Scope.DAILY) .status(true) .title("OPEN") - .openAt(now.minusMinutes(1)) - .lockAt(now.plusMinutes(10)) + .openAt(fixedTime.minusMinutes(1)) + .lockAt(fixedTime.plusMinutes(10)) .build(); } private BetRound closedRoundNow() { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime fixedTime = LocalDateTime.of(2025, 10, 13, 12, 0); return BetRound.builder() .betRoundID(roundId) .scope(Scope.DAILY) .status(true) .title("CLOSED") - .openAt(now.minusMinutes(10)) - .lockAt(now.minusMinutes(1)) + .openAt(fixedTime.minusMinutes(10)) + .lockAt(fixedTime.minusMinutes(1)) .build(); }
270-277: 무료 베팅 시 stakePoints 무시 로직 검증을 추가하세요.Line 275의 주석에서 stakePoints가 무시되어야 한다고 명시하고 있지만, 이를 명시적으로 검증하는 테스트가 없습니다.
postUserBet_free_success테스트에 다음 검증을 추가하는 것을 고려하세요:@Test @DisplayName("postUserBet: 무료 베팅 성공 → 포인트 차감 호출 안함, stake=0") void postUserBet_free_success() { BetRound round = openRoundNow(); when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round)); when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(false); when(userBetRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); UserBetRequest req = freeReq(); // stakePoints=999 전달 UserBet result = bettingService.postUserBet(userId, req); assertThat(result.isFree()).isTrue(); assertThat(result.getStakePoints()).isZero(); // ✓ 이미 검증됨 // 추가: 입력된 999가 무시되고 0으로 설정되었음을 명시적으로 확인 assertThat(req.getStakePoints()).isEqualTo(999); // 요청 객체는 변경되지 않음 assertThat(result.getStakePoints()).isNotEqualTo(req.getStakePoints()); // 결과는 다름 verify(pointHistoryService, never()).createPointHistory(any(), anyInt(), any(), any(), any()); verify(userBetRepository).save(any(UserBet.class)); }
279-364: postUserBet 테스트 커버리지가 우수합니다.성공 케이스(유료/무료)와 실패 케이스(라운드 없음, 중복, 시간 무효)를 모두 검증하고 있으며, 포인트 서비스 호출 여부를 적절히 확인합니다.
더 엄격한 검증을 위해 저장되는
UserBet객체의 필드를 명시적으로 확인하는 것을 고려하세요:@Test @DisplayName("postUserBet: 유료 베팅 성공 → 포인트 차감 호출 + 저장") void postUserBet_paid_success() { BetRound round = openRoundNow(); when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round)); when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(false); when(userBetRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); UserBetRequest req = paidReq(100); UserBet result = bettingService.postUserBet(userId, req); assertThat(result.getStakePoints()).isEqualTo(100); assertThat(result.isFree()).isFalse(); // 추가 검증 assertThat(result.getUserId()).isEqualTo(userId); assertThat(result.getRound()).isEqualTo(round); assertThat(result.getOption()).isEqualTo(BetOption.RISE); verify(pointHistoryService).createPointHistory( eq(userId), eq(-100), eq(PointReason.BETTING), eq(PointOrigin.BETTING), eq(roundId) ); // ArgumentCaptor를 사용한 더 엄격한 검증 ArgumentCaptor<UserBet> captor = ArgumentCaptor.forClass(UserBet.class); verify(userBetRepository).save(captor.capture()); UserBet saved = captor.getValue(); assertThat(saved.getStakePoints()).isEqualTo(100); assertThat(saved.isFree()).isFalse(); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java(3 hunks)backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java(1 hunks)backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java(2 hunks)backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java(1 hunks)backend/src/main/java/org/sejongisc/backend/point/service/PointHistoryService.java(2 hunks)backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java(3 hunks)backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java (4)
backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java (1)
Builder(10-15)backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java (1)
Getter(14-29)backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java (1)
Entity(11-59)backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java (1)
Entity(11-33)
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (1)
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java (1)
RequiredArgsConstructor(14-30)
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (1)
backend/src/main/java/org/sejongisc/backend/point/service/PointHistoryService.java (1)
Service(33-104)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Backend Gate
🔇 Additional comments (10)
backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java (1)
24-25: @JsonProperty 사용이 적절합니다
free필드를 JSON의isFree로 매핑하는 것은 명명 규칙을 명확히 하고 클라이언트와의 계약을 개선합니다.backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java (1)
4-7: Lombok 어노테이션 추가가 적절합니다엔티티에
@Getter,@Builder,@NoArgsConstructor,@AllArgsConstructor를 추가하여 보일러플레이트 코드를 제거하고, 서비스 레이어와 테스트 코드에서 사용하기 편리하게 개선되었습니다. 코드베이스의 다른 엔티티들(BetRound,Stock)과도 일관성 있는 패턴입니다.Also applies to: 13-14
backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java (1)
10-12: LGTM!리포지토리 메서드들이 Spring Data JPA 명명 규칙을 따르고 있으며,
existsByRoundAndUserId는 중복 베팅 방지에,findByUserBetIdAndUserId는 소유권 검증에 적절히 사용되고 있습니다.backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (1)
56-61: LGTM!베팅 관련 에러 코드들이 명확한 메시지와 적절한 HTTP 상태 코드로 추가되었습니다. 각 에러 시나리오(라운드 미존재, 중복 베팅, 시간 제약, 베팅 미존재, 라운드 마감)가 잘 표현되어 있습니다.
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (1)
86-128: 전반적인 로직이 견고합니다
postUserBet메서드가 라운드 검증, 중복 방지, 시간 제약, 포인트 처리 등을 체계적으로 수행하고 있습니다. 트랜잭션 경계도 적절히 설정되어 있습니다.backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (3)
51-59: LGTM!새로운
postUserBet엔드포인트가 인증(@AuthenticationPrincipal)과 검증(@Valid)을 적절히 적용하고 있으며, 비즈니스 로직은 서비스 레이어에 위임하여 관심사의 분리가 잘 되어 있습니다.
61-68: LGTM!
cancelUserBet엔드포인트가 DELETE 메서드에 적합한 204 No Content를 반환하고 있으며, 인증 처리가 올바르게 구현되어 있습니다.
31-31: API 경로 명명 규칙 개선이 적절합니다기존의 단수형
/bet-round에서 복수형/bet-rounds로 변경하여 RESTful API 명명 규칙을 따르도록 개선되었습니다.Also applies to: 44-44
backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java (2)
42-59: 테스트 설정이 올바르게 구성되었습니다.새로운 의존성(UserBetRepository, PointHistoryService)이 정확하게 모킹되고 주입되었으며, 테스트 격리가 @beforeeach를 통해 적절히 유지됩니다.
366-434: cancelUserBet 테스트가 주요 시나리오를 적절히 커버합니다.환불 금액, 삭제 호출, 실패 시 부작용 없음을 올바르게 검증합니다.
createPointHistory의originId로userBetId를 전달하는 것이 의도된 동작이므로 변경이 필요 없습니다.Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
Changes
Tests